Skip to content

Add CI job priority system#2217

Merged
adibarra merged 37 commits into
mainfrom
feat/ci-job-priority
Jul 22, 2026
Merged

Add CI job priority system#2217
adibarra merged 37 commits into
mainfrom
feat/ci-job-priority

Conversation

@adibarra

@adibarra adibarra commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Priority-aware CI scheduling

GitHub Actions does not natively prioritize self-hosted jobs. This PR adds an opt-in controller that decides which queued GPU job each compatible idle runner may accept.

Key mechanism: GitHub uses AND matching for runs-on labels. A runner must have every label requested by a job.

How dispatch works

With priority scheduling enabled, a job requests labels similar to:

  • self-hosted
  • b200
  • ci-job-6.000-a81f23...

Runners permanently carry only their normal hardware and identity labels. They do not permanently carry ci-job-* labels.

The controller:

  1. Reads queued jobs and online idle runners.
  2. Parses the score and queue hash from each job's ci-job-<score>-<hash> label.
  3. Adds queue aging and sorts higher scores first.
  4. Finds a compatible runner using existing hardware labels.
  5. Temporarily adds the selected job's combined ci-job-* label.
  6. Lets GitHub dispatch the now-exact label match.
  7. Removes stale scheduling labels after the runner becomes idle.

The combined label transports both the human-readable score and a unique 128-bit job hash through GitHub's queued-job API. Because every label must match, the hash prevents GitHub from selecting another job with the same hardware and priority.

Running jobs are never preempted or relabeled.

Single-node and multinode awareness

Existing runner labels remain compatibility constraints:

  • Single-node example: b200
  • Multinode example: b200-multinode

A single-node runner cannot accept a multinode job unless it already satisfies the multinode label. The controller schedules the coordinating GitHub runner; existing Slurm launchers continue provisioning worker nodes.

Automatic scoring

All ordinary priorities are numeric, nonnegative, automatic, and uncapped. Higher scores run first.

For priority-enabled sweep runs, the priority classifier inspects the relevant change range and returns only verified priority facts, such as topology, workload type, precision, speculative decoding, framework, model family, checklist completion, and patchwork. For pushes it compares the preceding commit to the pushed tip; checklist completion remains PR-only.

Fable never receives weights and never calculates a score. ci_priority.py:

  1. Rejects unknown facts.
  2. Intersects the facts with each matrix job's actual configuration.
  3. Reads weights from configs/ci-priority.yaml.
  4. Performs deterministic arithmetic.
  5. Emits the combined ci-job-<score>-<hash> label metadata.

This intersection keeps mixed matrices correct: a PR may contain both vLLM and SGLang jobs, but each job receives only its matching criteria. Main-branch pushes receive the same deterministic matrix-field scoring plus diff-derived classifier criteria.

Malformed or unavailable Fable output conservatively classifies the PR as patchwork. Arbitrary ci-priority:pN overrides are not supported.

Ties are resolved by queue timestamp and job ID. Runner selection preserves scarce exact-match runners when a less-constrained runner can accept the higher-priority job.

Job hashes

Generated job hashes are the first 128 bits of a SHA-256 digest over:

  • Workflow run namespace
  • Matrix path/index
  • Canonical job configuration

Manual matrix workflows derive the same 32-character hash from their run and matrix-cell identity. Hashes are routing identifiers, not credentials or secrets.

A score alone could release any compatible job with that priority. ci-job-<score>-<hash> combines ordering and one-job identity in a single exact-match label.

Protected skip_queue

skip_queue is the only absolute bypass mechanism. The workflow keeps the job's ordinary numeric priority and adds a PR-scoped skip request label.

The trusted controller then:

  1. Reads the PR timeline.
  2. Finds the active label's latest labeling actor.
  3. Verifies active SemiAnalysisAI/Core membership.
  4. Treats the job's effective priority as absolute only when authorized.

Unauthorized or unverifiable requests retain their numeric priority. The scheduler credential remains inside the trusted controller and is never passed to PR-controlled workflow code.

Required controller-token permissions:

  • Actions: Read
  • Administration: Write
  • Issues: Read
  • Organization Members: Read

Safety and failure behavior

  • Busy and offline runners are never relabeled.
  • Malformed or duplicate scheduler labels fail loudly.
  • Transient GitHub API failures retry in watch mode.
  • Partial label updates converge on the next controller cycle.
  • Controller downtime leaves priority-enabled jobs queued.
  • Run exactly one active controller instance.
  • Existing routing remains unchanged while disabled.

Activation requires deploying the controller and setting:

PRIORITY_SCHEDULER_ENABLED=true

Verification

  • 28 focused priority tests cover scoring, rounding, aging, authorization, malformed labels, compatibility, exact-runner preservation, and dispatch.
  • 27 sweep-gating tests cover named cases and the exhaustive event/label cross-product.
  • The complete changelog and sweep-gating CI suite passes: 138 tests.
  • Changed workflows pass actionlint.
  • Simulations verified automatic numeric scoring and unique queue tokens.
  • Core-authorized skip_queue outranked numeric priority 100.000 while retaining its numeric job label.

@adibarra
adibarra marked this pull request as ready for review July 22, 2026 08:20
@adibarra
adibarra requested a review from a team July 22, 2026 08:20
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Claude finished @adibarra's task in 13m 28s —— View job


Review of PR #2217 — CI job priority system

  • Gather context (diff vs origin/main, AGENTS.md)
  • Review utils/ci_priority.py and tests
  • Review workflow changes (run-sweep.yml, templates, canary, e2e, profile, speedbench, collectivex)
  • Review configs/ci-priority.yaml and gating test changes
  • Verify test suites via CI (local pytest/actionlint execution not permitted in this sandbox; the Test Changelog Gate run on this branch is green, which covers test_ci_priority.py and the gating suites)
  • Post findings

Findings

1 blocking, 1 warning — posted as inline comments:

  • 🔴 speedbench-al.yml:133jq -r '.[0][\"queue-token\"]' is a jq syntax error (backslashes are literal inside single quotes; verified: jq exits 3 with unexpected INVALID_CHARACTER). The step still passes, so queue-token is silently empty and, once the scheduler is enabled, collect-al requests the malformed label ci-job-<score>- and never dispatches. One-character fix suggested inline.
  • 🟡 run-sweep.yml classify-priority — the gate is looser than setup's, so a Fable classification runs (and is discarded) on changelog-touching PRs with no primary sweep label, on [skip-sweep] heads, and on unrelated labeled/unlabeled events where check-changelog is skipped. Suggest mirroring setup's primary-label + skip-pr-sweep checks.

Checked and found sound

  • Scoring is deterministic (Decimal + half-up quantize), unknown Fable criteria are rejected, patchwork fallback on malformed classifier output, and no ci-priority:pN override path exists — matches the PR description.
  • The with: *single-node-inputs / *multi-node-inputs YAML anchors carry the new priority/queue-token/skip-queue-pr inputs to the 8k1k call sites, so all 10 run-sweep and 7 e2e-tests template calls satisfy the new required: true inputs.
  • annotate_jobs leaves changelog_metadata untouched (only entries with both runner and framework are annotated), so the ingest artifact and the reuse tooling (which never compares search-space-config across runs) are unaffected.
  • p<score> | job-name prefixes don't break calc_success_rate.py (it substring-matches hardware labels).
  • Per-family queue namespaces in e2e-tests.yml prevent token collisions between matrices; --labels-json handles the null labels payload on push events.
  • Everything is inert while PRIORITY_SCHEDULER_ENABLED != 'true' (fallback runs-on equals current behavior).

Notes (non-blocking)

  • Adding/removing any of the new metadata labels (ci-checklist-complete, ci-patchwork, ci-patchwork-waived, engine-patch, skip_queue) lands in the sweep-<pr>-active concurrency group with cancel-in-progress: true — labeling ci-checklist-complete (+0.25) mid-run cancels an in-flight multi-hour sweep and restarts it from scratch. The gating tests name this "restarts-full-sweep", so it appears intentional; flagging it since it's an expensive side effect for a small score bump.
  • The two disagg: 'false'disagg: ${{ 'false' }} edits in e2e-tests.yml are no-ops (the input is type: string) — looks like leftover churn worth reverting.
  • speedbench-al.yml's setup hardcodes precision: fp4 / spec-decoding: mtp regardless of mtp-list — fine if the fixed score is intended for AL collection, but it's fabricated job metadata if not.

中文:审查发现 1 个阻塞问题(speedbench-al.yml 第 133 行的 jq 语法错误导致 queue-token 静默为空,调度器启用后作业将永远无法派发)和 1 个警告(classify-priority 的触发条件过宽,会在不会执行 sweep 的 PR 事件上浪费 Fable 分类调用)。其余核心逻辑(确定性打分、YAML 锚点传递新输入、changelog_metadata 不受影响、禁用时行为不变)均已核查无误。另请注意:在运行中添加 ci-checklist-complete 等元数据标签会取消并重启整个 sweep。

Comment thread .github/workflows/speedbench-al.yml Outdated
Comment thread .github/workflows/run-sweep.yml Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the two inline findings, I also checked whether the setup job's PR-controlled invocation of ci_priority.py lets a PR game its own CI priority score — it doesn't: the score is computed strictly from the job's own matrix fields (framework/precision/spec-decoding/etc., which already determine what actually runs) intersected with repo-owned weights in configs/ci-priority.yaml, and there is no ci-priority:pN override, so a PR can't inflate its priority beyond what its own changelog entry already implies.

Extended reasoning...

Ruled out a candidate self-scoring/gaming concern raised by a finder agent for the setup job in run-sweep.yml: ci_priority.py's inputs (matrix config fields, Fable criteria, GH-event labels) are either derived from the job's real configuration or from trusted sources, and there's no arbitrary-override path (no ci-priority:pN support), so a PR cannot inflate its own priority beyond what its declared job configuration legitimately implies.

Comment thread .github/workflows/speedbench-al.yml Outdated
Comment thread .github/workflows/run-sweep.yml Outdated
@adibarra
adibarra merged commit 1015a20 into main Jul 22, 2026
4 checks passed
@adibarra
adibarra deleted the feat/ci-job-priority branch July 22, 2026 21:57
@adibarra adibarra changed the title [WIP] Add CI job priority system Add CI job priority system Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant